這一篇要正式進入「HTTP 回應」環節,也就是第三小節。
本節將透過 4 篇文章,介紹 Django Ninja 如何處理 HTTP 回應:
我們會講述更多 Schema 用法,透過這些技巧,你能夠精確地控制 API 的輸出格式。無論是單一物件回應,還是複雜的嵌套結構,接下來都會一一提及。
本文所有的程式碼變動,可參考這個 PR。
本文將一步一步,從簡單到複雜,介紹如何透過 Django Ninja 建立 HTTP 回應。
並且用既有的 3 個 API 進行示範(會依需求為它們增補不同內容):
response=
參數。開始吧!
先來看最簡單的回應格式,這個例子會展示如何回應一個 Python 字典,並手動設定 HTTP 回應狀態碼。
以「新增文章」API 為例:(省略部分程式碼)
@router.post(path='/posts/')
def create_post(...) -> dict:
...
return {'id': post.id, 'title': post.title}
這裡回應的是一個 Python 字典,事實上,你可以 return「任何能夠 JSON 序列化」的 Python 資料。(所以 Django 模型物件不行,因為它無法直接序列化)
因此,以下這些都可以 return:
"Hello World !"
[1 , 2 , 3]
{"name": "Alice", "age": 30, "hobbies": ["reading", "swimming"]}
這些都會被 Django Ninja 自動序列化為 JSON 格式,並作為 API 的回應。
{
"id": 666,
"title": "How to Be a Ninja"
}
View 函式處理回應,往往要加入 HTTP 狀態碼。尤其在有多種回應狀態的時候,需要透過狀態碼來區分。
做法很簡單,就是在回應的內容前面直接加上:
return 201, {'id': post.id, 'title': post.title}
如此一來,函式的回傳型別就從原來的dict
變成tuple
了。
所以我們函式簽名的 type hints 也要跟著修正:
def create_post(...) -> tuple[int, dict]:
如果你沒有加前面這個狀態碼數字,Django Ninja 就將其預設為 200。
值得注意的是,當你的 view 函式要 return「非 200」回應時,必須在router
裝飾器聲明:
@router.post(path='/posts/', response={201: dict}) # 這裡
response={201: dict}
就是聲明的方式,採用 Python 字典來一一對應狀態碼與回傳內容格式。
創作當時,這部分的範例專案程式碼還未補上,所以這個 API 無法正常回應😅,特此提醒。
上述第一種回應很簡單,不過大部分 API 回應都沒這麼單純。
我們來看第二種回應。
開發 Django API,回應中的資料,有很大部分是從 Django 模型物件序列化而來。
但通常我們不會直接將資料庫中的所有資訊傳送給前端。相反,我們會進行欄位篩選、驗證或格式轉換。
這樣不僅能夠精確控制 API 的輸出,還能確保資料的正確與安全性。
Django Ninja 中,這些「篩選、驗證、格式轉換」等需求,都是透過 Schema 實現。
我們來為「單得取一文章」API 設計一個回應格式,使用 Schema。
# post/schemas.py
from datetime import datetime
...
class PostResponse(Schema):
id: int
title: str
content: str
author_id: int
created_at: datetime
updated_at: datetime
這個PostResponse
Schema 包含了Post
幾乎所有的欄位。
注意,Schema 定義將決定輸出的欄位。如果 Schema 中只有id
一欄,那輸出結果就只會有該欄的資料。
接著,我們在 view 函式中使用這個 Schema:
@router.get(path='/posts/{int:post_id}/', response=PostResponse)
def get_post(request: HttpRequest, post_id: int) -> Post:
"""
取得單一文章
"""
post = Post.objects.get(id=post_id)
return post
只有改一行!——在router
裝飾器加上response=PostResponse
。
有了response=PostResponse
設定,Django Ninja 會將函式回傳的Post
模型物件,丟給PostResponse
進行驗證,成功之後直接轉為 JSON 格式並送回前端。
看看回應結果:
// http://127.0.0.1:8000/posts/2/
{
"id": 2,
"title": "Alice's Django Ninja Post 1",
"content": "Alice's Django Ninja Post 1 content",
"author_id": 1,
"created_at": "2024-09-12T02:28:16.801Z",
"updated_at": "2024-09-12T02:28:16.801Z"
}
非常好!
「清單、列表」也是 API 的常見回應形態,包含多筆資料。
我們繼續使用剛剛的PostResponse
,不作任何更動,直接套用在「取得文章列表」這個 API。
一樣,只要更改一行即可,但與前面略有不同:
@router.get(path='/posts/', response=list[PostResponse])
我們使用了list[PostResponse]
,表示回應會是一個PostResponse
物件的 list。
然而實際上,此時你不需要「真的」return 一個 Python list,可以直接回傳 QuerySet 就好,Django Ninja 會自行處理物件的迭代與序列化。
甚至,只要你 return 的是一個 iterable,而且 iterable 中的每一個元素,都能夠通過PostResponse
驗證(符合格式),那就足夠了!
來看看結果,因為列表太長了,我改用截圖呈現:
上面提到的回應,不是 200 就是 201,但通常 API 往往還會有 400、401、403 甚至 500 等回應,如何處理它們之間的對應關係?
沒錯,就是擴大response=
中的字典!我們直接看官方文件的例子:
class Token(Schema):
token: str
expires: date
class Message(Schema):
message: str
@api.post('/login', response={200: Token, 401: Message, 402: Message})
...
值得留意的是,字典的 key 不可重複,但值可以!——Message
出現了兩次。
但我覺得這個「多重狀態碼回應」設定在實務上沒有很實用,為何?我們後續再談。
本文中,我們從最簡單的回應開始,逐步介紹了如何在回應中返回單一和多筆資料,並提到了 Django Ninja 如何設定多重狀態碼回應。
下一篇將探討,如何處理回應中複雜的巢狀結構,讓我們的 API 愈來愈健全。
本文同步發表於我的部落格——Code and Me
如果裝飾器上的 response 引數沒填,會用 type hint 的值來驗證嗎? FastAPI 是會,Ninja 我沒讀文件則不確定。(不過目前看到這裡為止,Ninja 真的跟 FastAPI 超像!)
「多重狀態碼回應」很沒用的原因我來猜一下。
感覺就是 call API 的人多半不在乎 200 以外的內容為何,或者更正確地說,是對方的程式只要知道失敗就好,這些 2XX 以外的詳細錯誤訊息多半是人看而不是機器看的。
如果每種錯誤格式都不一樣,每種都要寫個特殊的 schema 來接收這些結果但卻幾乎都用不到就真的沒什用XD
如果裝飾器上的 response 引數沒填,會用 type hint 的值來驗證嗎?
我還真不確定(沒想過),但我認為應該不會,畢竟很多人很多時候,根本沒有寫 return 的 type hints
讓「多重狀態碼回應」用法變得雞肋的原因,一言難盡,牽涉到 Django Ninja 的錯誤處理方式,還有大量使用裝飾器帶來的影響。我會試著在後續文章中說明這一點,只是有點挑戰,哈哈哈
我個人倒覺得 400、404、500 這些區別挺重要,我也很在乎錯誤回應的內容與合理性,只是不會用本文的「多重狀態碼回應」(也就是那個字典)來實踐而已